using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using Red.Controls.Notification;
using Red.Controls.Shell;
using Red.Core;

namespace Red.Toolkit.Host
{
    /// <summary>
    ///     Represents loaded editor plug-in
    /// </summary>
    internal class Editor
    {
        [Import( typeof( IEditorInfo ) )]
        private IEditorInfo _info = null; // had to assign default val to shut up the compiler

        private readonly Assembly _assembly;

        public string DllName { get; private set; }
        public EditorInstanceData InstanceData { get; private set; }
        public RedEditorWindow MainWindow { get; private set; }

        internal static async Task<Editor> CreateAsync( string editorDllName, Application host, EditorInstanceData instanceData, EditorSharedData sharedData )
        {
            var name = AssemblyName.GetAssemblyName( editorDllName );
            var assembly = await Task.Run<Assembly>( () => Assembly.Load( name ) ); // load on a separate thread
            return new Editor( assembly, host, instanceData, sharedData ) { DllName = editorDllName };
        }

        private Editor( Assembly assembly, Application host, EditorInstanceData instanceData, EditorSharedData sharedData )
        {
            _assembly = assembly;

            AssemblyCatalog catalog = new AssemblyCatalog( _assembly );
            CompositionContainer container = new CompositionContainer( catalog );
            container.SatisfyImportsOnce( this );

            instanceData.EditorName = _info.Name;
            InstanceData = instanceData;
            MainWindow = (RedEditorWindow)Activator.CreateInstance( _info.WindowClass, new object[] { host, instanceData, sharedData } );
            MainWindow.Settings = (RedSettings)Activator.CreateInstance( _info.SettingsClass );
           
            // NOTE: Once an assembly is loaded, it cannot be unloaded, unless put into separate AppDomains, which
            // also improves stability, but complicates data sharing. Maybe later.
        }
    }

    /// <summary>
    ///     Manages instances of open plug-in editors
    /// </summary>
    internal class EditorManager
    {
        #region Data
        
        private readonly List<Editor> _editors = new List<Editor>();
        private readonly HostApp _host;
        
        #endregion Data

        #region C-tor
        
        internal EditorManager( HostApp host )
        {
            _host = host;
        }
        
        #endregion C-tor

        #region Public methods
        
        /// <summary>
        ///     If there is an editor with the same dllName and instanceContext, it's brought to front.
        ///     If not, a new instance is created and shown.
        /// </summary>
        /// <param name="dllName">
        ///     Dll name of a editor plug-in to be loaded
        /// </param>
        /// <param name="instanceData">
        ///     Data specific for launched editor instance.
        /// </param>
        /// <param name="sharedData">
        ///     Data shared by all editors.
        /// </param>
        /// <returns>
        ///     Status of the operation
        /// </returns>
        public async Task<Result<RedEditorWindow>> LaunchEditorAsync( 
            string dllName, string instanceContext, CommandLineDictionary additionalParams, EditorKind kind, EditorSharedData sharedData 
            )
        {
            Editor existingEditor = _editors.FirstOrDefault( e => e.DllName == dllName && e.InstanceData.InstanceContext == instanceContext );
            if ( existingEditor != null )
            { 
                // existing editor found - bring to front
                existingEditor.MainWindow.Activate();
                return Result<RedEditorWindow>.Ok( existingEditor.MainWindow );
            }

            try
            {
                var instanceData = new EditorInstanceData( instanceContext, additionalParams, kind );
                var newEditor = await Editor.CreateAsync( dllName, _host, instanceData, sharedData );
                _editors.Add( newEditor );
                instanceData.InitializeNotificationDispatcher( new NotificationDispatcher( newEditor.MainWindow ) );

                // TODO: handle the editable argument here by enabling/disabling editor's window controls
                newEditor.MainWindow.Closed += MainWindow_Closed;
                newEditor.MainWindow.Show();
                return Result<RedEditorWindow>.Ok( newEditor.MainWindow );
            }
            catch ( Exception ex )
            { // something went wrong, try to recover
                return Result<RedEditorWindow>.Failed( ex.Message );
            }
        }

        /// <summary>
        ///    Launches editor bound to the given resource. The resource has to be under the depot root.
        /// </summary>
        /// <param name="path">
        ///     Absolute local path to a resource
        /// </param>
        /// <param name="sharedData">
        ///     Data shared by all editors.
        /// </param>
        /// <returns>
        ///     Status of the operation
        /// </returns>
        public async Task<Result<RedEditorWindow>> EditResourceAsync( string path, EditorSharedData sharedData )
        {
            var depotPath = sharedData.DepotInfo.GetDepotPath( path );
            if ( depotPath == null )
            {
                return Result<RedEditorWindow>.Failed( "Resource is not under depot root" );
            }

            var ext = System.IO.Path.GetExtension( depotPath );
            ResourceTypeInfo info = ResourceTypeInfo.GetForExtension( ext.Substring( 1 ) );

            if ( info == null || info.Editor == null )
            {
                bool result = false;
                await Task.Run( () => {
                    result = Process.Start( path ) != null;
                } );
                return result ? Result<RedEditorWindow>.Ok( null ) : Result<RedEditorWindow>.Failed( "Unknown resource binding for" + ext );
            }

            string editor = info.Editor + ".dll";

            var additionalParams = new CommandLineDictionary(); // empty for now
            var kind = info.UsesEngine ? EditorKind.EngineResource : EditorKind.StandaloneResource;

            try
            {
                return await LaunchEditorAsync( editor, depotPath, additionalParams, kind, sharedData );
            }
            catch ( Exception ex )
            {
                return Result<RedEditorWindow>.Failed( ex.Message );
            }
        }

        #endregion Public methods

        #region Event handlers
        
        private async void MainWindow_Closed( object sender, EventArgs e )
        {
            var editor = _editors.FirstOrDefault( ed => ed.MainWindow == sender );
            if ( editor != null )
            {
                if ( editor.InstanceData.RootEditorItem != null )
                {
                    try
                    {
                        // close editor on the engine side
                        await editor.InstanceData.RootEditorItem.ExecuteAsync( "close" );
                    }
                    catch ( Exception )
                    {
                        // for now it's just silenced
                    }
                }
                _editors.Remove( editor );
            }
        }
        
        #endregion Event handlers
    }
}
